import java.math.BigInteger
import java.util.Stack

import com.sun.imageio.plugins.common.ImageUtil.isBinary

object Evaluator {

    private val EPSILON = 1e-15
    private var deg = false

    /**
     * Calculates the the Post Fix expression (Reverse polish expression) and if the
     * result is less than EPSILON then it returns zero
     *
     * @param expression A mathematical expression
     * @return the result of the expression
     * @throws Exception if and Operand is missing, divide by zero ect...
     */
    @Throws(Exception::class)
    fun evaluate(expression: String): String {
        val postFix = Parser.toPostFix(expression)
        val stack = Stack<String>()
        for (s in postFix) {
            if (Parser.isOperand(s)) {// if the current token is a number push it to the stack
                stack.push(s)
            } else {// if token is an operator pop the stack according to the operators arity
                // (Binary or Unary)
                if (Parser.isBinary(s) === Parser.BINARY) {
                    if (stack.size < 2)
                        throw EvaluatorException("Missing operand")
                    val b = java.lang.Double.parseDouble(stack.pop())
                    val a = java.lang.Double.parseDouble(stack.pop())
                    stack.push(BinaryEval(a, b, s).toString())
                } else {
                    if (stack.size < 1)
                        throw EvaluatorException("Missing operand")
                    val a = java.lang.Double.parseDouble(stack.pop())
                    stack.push(UnaryEval(a, s).toString())
                }
            }
        }
        val result = java.lang.Double.parseDouble(stack.pop())

        return evalError(result).toString()
    }

    @Throws(Exception::class)
    fun evaluate(expression: String, deg: Boolean): String {// TODO turn off scientific notation
        setDeg(deg)
        return evaluate(expression)
    }

    @Throws(EvaluatorException::class)
    private fun BinaryEval(a: Double, b: Double, op: String): Double {
        if (op == "+")
            return a + b
        if (op == "-") {
            return a - b
        }
        if (op == "*")
            return a * b
        if (op == "/") {
            if (b == 0.0)
                throw EvaluatorException("Divide by 0")
            return a / b
        }
        if (op == "^")
            return Math.pow(a, b)

        throw IllegalArgumentException("Operation not found")// if non of the above where executed throw this
    }

    @Throws(EvaluatorException::class)
    private fun UnaryEval(a: Double, op: String): Double? {
        if (op == "$")
            return -a

        if (op == "cos")
            return if (deg) Math.cos(Math.toRadians(a)) else Math.cos(a)

        if (op == "sin")
            return if (deg) Math.sin(Math.toRadians(a)) else Math.sin(a)

        if (op == "tan")
            return if (deg) Math.tan(Math.toRadians(a)) else Math.tan(a)

        if (op == "arccos")
            return if (deg) Math.acos(Math.toRadians(a)) else Math.acos(a)

        if (op == "arcsin")
            return if (deg) Math.asin(Math.toRadians(a)) else Math.asin(a)

        if (op == "arctan")
            return if (deg) Math.atan(Math.toRadians(a)) else Math.atan(a)
        // ln, log and square root will return NaN if a is less than 0
        if (op == "ln")
            return Math.log(a)

        if (op == "log")
            return Math.log10(a)

        if (op == "√")
            return Math.sqrt(a)

        if (op == "!") {
            if (a > 120)
                throw EvaluatorException("Number too big")
            if (isInt(a))
                return factorial(a.toLong())

            throw EvaluatorException("Can't use factorial on floats")
        }
        return if (op == "%") a / 100 else 0.0
    }

    private fun evalError(number: Double?): Double? {
        return if (Math.abs(Math.floor(number!!) - number) < EPSILON) Math.floor(number) else number
    }

    /**
     * Standard recursive factorial function but with BigInteger for more
     * possibilities
     */
    private fun recfact(n: Long): BigInteger {
        return if (n == 0L) BigInteger.ONE else BigInteger.valueOf(n).multiply(recfact(n - 1))
    }

    private fun factorial(n: Long): Double {// TODO Tweak the retured factorial format for big numbers
        // NumberFormat formatter = new DecimalFormat("0.######E0",
        // DecimalFormatSymbols.getInstance(Locale.ROOT));
        // String str = formatter.format(recfact(n));
        return recfact(n).toDouble()
    }

    /**
     * Checks if the number is an integer number
     *
     * @param number a number
     * @return true if number is an integer or false if it is a float
     */
    private fun isInt(number: Double): Boolean {
        return number == Math.floor(number) && !java.lang.Double.isInfinite(number)
    }

    fun setDeg(deg: Boolean) {
        Evaluator.deg = deg
    }

    class EvaluatorException(message: String) : Exception(message) {
        override fun getStackTrace(): Array<StackTraceElement> {
            return super.getStackTrace()
        }

        override fun printStackTrace() {
            super.printStackTrace()
        }
    }
}
